package com.ihy.config;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.ihy.constant.FastJson2JsonRedisSerializer;
import com.ihy.utils.RedisTemplateUtil;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * redis配置类
 * <p>
 * Configuration注解：
 * 指示类声明一个或多个@Bean方法，并可由Spring容器处理以在运行时为这些Bean生成Bean定义和服务请求.
 * <p>
 * EnableCaching注解：
 * 启用Spring的注解驱动缓存管理功能，类似于Spring的<cache:*>XML命名空间中的支持.
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    /**
     * @param redisTemplate 简化Redis数据访问代码的Helper类
     * @return Redis模板工具类
     * <p>
     * Bean注解：
     * 指示一个方法生成一个由Spring容器管理的bean。
     * 此注解的属性的名称和语义有意地与Spring Xml Schema中的<bean/>元素的名称和语义相似
     * <p>
     * RedisTemplate类：
     * 简化Redis数据访问代码的Helper类。
     * 在给定对象和Redis存储中的底层二进制数据之间执行自动序列化/反序列化。默认情况下，它对其对象使用Java序列化（通过JdkSerializationRedisSerializer）。对于字符串密集型操作，请考虑专用的StringRedisTemplate。
     * 中心方法是execute，支持Redis访问代码实现RedisCallback接口。它提供了RedisConnection处理，这样RedisCallback实现和调用代码都不需要显式地关心检索/关闭Redis连接，或者处理连接生命周期异常。对于典型的单步动作，有各种方便的方法。
     * 一旦配置，这个类是线程安全的。
     * 注意，在模板被泛化的同时，由序列化程序/反序列化程序来正确地将给定对象与二进制数据进行转换。
     */
    @Bean
    public RedisTemplateUtil redisTemplateUtils(RedisTemplate<String, Object> redisTemplate) {
        //redis模板工具类
        RedisTemplateUtil redisTemplateUtil = new RedisTemplateUtil();
        //set方法设置redisTemplate
        redisTemplateUtil.setRedisTemplate(redisTemplate);
        //返回当前
        return redisTemplateUtil;
    }

    /**
     * 生成key的策略 根据类名+方法名+所有参数的值生成唯一的一个key
     *
     * @return 主键
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();
        };
    }

    /**
     * 管理缓存
     *
     * @param redisConnectionFactory Redis连接的线程安全工厂
     * @return cacheManager
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 通过Spring提供的RedisCacheConfiguration类，构造一个自己的redis配置类，从该配置类中可以设置一些初始化的缓存命名空间
        // 及对应的默认过期时间等属性，再利用RedisCacheManager中的builder.build()的方式生成cacheManager：
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 生成一个默认配置，通过config对象即可对缓存进行自定义配置
        // 设置缓存的默认过期时间，也是使用Duration设置
        config = config.entryTtl(Duration.ofMinutes(3))
                // 不缓存空值
                .disableCachingNullValues();
        // 设置一个初始化的缓存空间set集合
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("redisCacheNoTime");
        cacheNames.add("redisCacheTime");
        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("redisCacheNoTime", config);
        configMap.put("redisCacheTime", config.entryTtl(Duration.ofSeconds(1200)));
        // 使用自定义的缓存配置初始化一个cacheManager
        return RedisCacheManager.builder(redisConnectionFactory)
                // 注意这两句的调用顺序，一定要先调用该方法设置初始化的缓存名，再初始化相关的配置
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .build();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //设置连接工厂
        template.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值（默认使用JDK的序列化方式）
        FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
        /*
          ObjectMapper提供了读写JSON的功能，既可以读写基本POJO（普通旧Java对象），也可以读写写通用JSON树模型（JsonNode），还提供了执行转换的相关功能。
          它还具有高度的可定制性，既可以使用不同样式的JSON内容，也可以支持更高级的对象概念，如多态性和对象标识。
          ObjectMapper还充当更高级的ObjectReader和ObjectWriter类的工厂。
          Mapper（以及它构造的ObjectReaders、ObjectWriters）将使用JsonParser和JsonGenerator的实例来实现JSON的实际读写。
          请注意，尽管大多数读写方法都是通过此类公开的，但有些功能仅通过ObjectReader和ObjectWriter公开：
          具体来说，较长的值序列的读写只能通过ObjectReader.ReadValue（输入流）和ObjectWriter.WriteValue（输出流）。
         */
        ObjectMapper mapper = new ObjectMapper();
        //一种方便的方法，允许更改基础VisibilityCheckers的配置，以更改自动检测哪些类型属性的详细信息。
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //相当于调用 启用DefaultTyping（dti，JsonTypeInfo.As.u数组);
        //注意：如果传入的内容来自不受信任的源，则使用默认类型可能会带来潜在的安全风险，建议不要这样做，或者如果启用，则使用setDefaultTyping传递自定义TypeResolverBuilder实现，该实现将白名单中要使用的合法类型。
        //mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        /*  enableDefaultTyping方法过期替换
         *
         *      DefaultTyping:实现通用的序列化和反序列化
         *          JAVA_LANG_OBJECT: 当对象属性类型为Object时生效
         *          OBJECT_AND_NON_CONCRETE: 当对象属性类型为Object或者非具体类型（抽象类和接口）时生效
         *          NON_CONCRETE_AND_ARRAYS: 同上, 另外所有的数组元素的类型都是非具体类型或者对象类型
         *          NON_FINAL: 对所有非final类型或者非final类型元素的数组
         *      JsonTypeInfo:作用于类/接口，被用来开启多态类型处理，对基类/接口和子类/实现类都有效
         *          Id.NONE：不使用识别码
         *          Id.CLASS：使用完全限定类名做识别，识别码名称@class
         *          Id.MINIMAL_CLASS：表示具有最小路径的Java类名称用作识别，识别码名称@c（慎用）
         *          Id.NAME：使用类型的名称做识别，识别码名称@type
         *          Id.CUSTOM：自定义识别码，需结合property属性和JsonTypeIdResolver注解
         *          As.PROPERTY：作为POJO的属性出现（默认）
         *          As.WRAPPER_OBJECT：作为一个包装的对象
         *          As.WRAPPER_ARRAY：作为一个包装的数组
         *          As.EXTERNAL_PROPERTY：作为一个额外的属性，跟POJO同级，只能用于属性；如何作用于类则跟JsonTypeInfo.As.PROPERTY是相同效果
         *          As.EXISTING_PROPERTY：反序列化的时候，跟JsonTypeInfo.As.PROPERTY的处理相同；序列化，则Jackson不主动处理，由我们自行处理。
         *          序列化：POJO -> JSON
         *          反序列化：JSON -> POJO
         */
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        /*
          设置此视图的ObjectMapper。如果未设置，则使用默认的ObjectMapper。
          设置自定义配置的ObjectMapper是进一步控制JSON序列化过程的一种方法。例如，可以配置扩展SerializerFactory，为特定类型提供自定义序列化程序。
          优化序列化过程的另一个选项是对要序列化的类型使用Jackson提供的注解，在这种情况下，不需要自定义配置的ObjectMapper。
         */
        serializer.setObjectMapper(mapper);
        //设置此模板要使用的值序列化程序。默认为getDefaultSerializer（）。
        template.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        //以字符串为中心的RedisTemplate扩展。因为大多数针对Redis的操作都是基于字符串的，所以这个类提供了一个专用的类，该类可以最小化其更通用的模板的配置，特别是在序列化程序方面。
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        //设置连接工厂
        stringRedisTemplate.setConnectionFactory(factory);
        return stringRedisTemplate;
    }
}